/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using VSLangProj;
using VSLangProj80;
using EnvDTE;
using DaveSexton.DocProject.Configuration;
using DaveSexton.DocProject.Engine;
using System.Collections.ObjectModel;
using DaveSexton.DocProject.Resources;
using System.ComponentModel;

namespace DaveSexton.DocProject
{
	public class VSDocProject : VSAnyProject, IDocProject
	{
		#region Public Properties
		public virtual IEnumerable<SystemPath> Dependencies
		{
			get
			{
				// The iterator pattern cannot be used here (i.e., the yield statement) because it causes the 
				// debugger to freeze, for some unknown reason.  But it won't improve the performance anyway
				// since the FindUniqueReferencesRecursive method does not use the iterator pattern either.
				return ReferenceResolver.FindUniqueReferencesRecursive(AllSources.Inputs);
			}
		}

		public virtual ICollection<ISourceProject> ProjectSources
		{
			get
			{
				List<ISourceProject> sources = new List<ISourceProject>();

				foreach (Reference reference in References)
				{
					if (IsSourceReference(reference, true))
						sources.Add(ProjectFactory.CreateSourceProject(reference.SourceProject));
				}

				return sources.AsReadOnly();
			}
		}

		public virtual SourceCollection ExternalSources
		{
			get
			{
				return externalSources;
			}
		}

		public virtual SourceCollection AllSources
		{
			get
			{
				SourceCollection sources = new SourceCollection(Provider.AllSupportedExternalSourceFileExtensions);

				foreach (ISourceProject project in ProjectSources)
					sources.Add(new Source(project.Output.OutputFile, project.Output.XmlDocumentationFile));

				sources.AddRange(externalSources);

				return sources.AsReadOnly();
			}
		}

		public DocProjectOptions Options
		{
			get
			{
				if (options == null)
				{
					options = Provider.CreateProjectOptions(this);

					if (options == null)
						throw new InvalidOperationException(Errors.CreateProjectOptionsReturnedNull);
				}

				return options;
			}
		}

		/// <remarks>
		/// This property is assignable for internal use only.  Never assign a value to this property.
		/// </remarks>
		public BuildEngineProvider Provider
		{
			get
			{
				if (provider == null)
				{
          // NOTE: Do not use GetCommittedValue since the Provider is loaded for new projects before the 
          // value is committed by the New Project Wizard; otherwise, the Active Projects page will use 
          // the DefaultBuildEngineProvider until Visual Studio is restarted.
          string providerName = Settings.Read("BuildEngineProviderName");

					if (!string.IsNullOrEmpty(providerName))
						provider = DocProjectEnvironment.GetProvider(providerName);

					if (provider == null)
						provider = DocProjectEnvironment.DefaultBuildEngineProvider;
				}

				return provider;
			}
			set
			{
        if (provider == value)
          return;

				provider = value;
        options = null;   // force the Options property to reload
			}
		}

		public virtual bool IsDocSite
		{
			[System.Diagnostics.DebuggerHidden]
			get
			{
				try
				{
					return Properties["WebApplication.ToString"] == "Microsoft.VisualStudio.Web.Application.WAProjectExtender";
				}
				catch (ArgumentException)
				// this exception is thrown for projects that aren't web applications, although the actual value of the property
				// is checked before true is returned anyway
				{
					return false;
				}
			}
		}
		#endregion

		#region Private / Protected
		private DocProjectOptions options;
		private BuildEngineProvider provider;
		private readonly SourceCollection externalSources;
		private readonly ReferencesEvents referenceEvents;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="VSDocProject" /> class.
		/// </summary>
		internal VSDocProject(VSProject2 vsProject2)
			: base(vsProject2)
		{
			externalSources = new SourceCollection(Provider.AllSupportedExternalSourceFileExtensions);
			externalSources.AddFromList(Directory, Settings.Read("ExternalSources"));
			externalSources.ItemAdded += externalSources_ItemAdded;
			externalSources.ItemRemoved += externalSources_ItemRemoved;

			referenceEvents = vsProject2.Events2.ReferencesEvents;
			referenceEvents.ReferenceAdded += referenceEvents_ReferenceAdded;
			referenceEvents.ReferenceChanged += referenceEvents_ReferenceChanged;
			referenceEvents.ReferenceRemoved += referenceEvents_ReferenceRemoved;
		}
		#endregion

		#region Methods
		[System.Diagnostics.DebuggerHidden]
		private static SystemPath? CreateSystemPath(Reference reference)
		{
			Reference3 ref3 = reference as Reference3;

			if (ref3 != null && (!ref3.Resolved || ref3.RefType != 1))		// 1=assembly; 2=COM; 3=Native
				return null;
			else
				return new SystemPath(reference.Path, false, false);
		}

		private static bool IsSourceReference(Reference reference, bool canUseCache)
		{
			Project source = reference.SourceProject;

			return source != null && ProjectFactory.IsSourceProject(source, canUseCache);
		}

		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				referenceEvents.ReferenceAdded -= referenceEvents_ReferenceAdded;
				referenceEvents.ReferenceChanged -= referenceEvents_ReferenceChanged;
				referenceEvents.ReferenceRemoved -= referenceEvents_ReferenceRemoved;
			}

			base.Dispose(disposing);
		}
		#endregion

		#region Events
		private readonly object ReferenceAddedEvent = new object();
		private readonly object ReferenceChangedEvent = new object();
		private readonly object ReferenceRemovedEvent = new object();

		/// <summary>
		/// Event raised when a reference is removed.
		/// </summary>
		[Category("Action")]
		[Description("Event raised when a reference is removed.")]
		public event EventHandler<SystemPathEventArgs> ReferenceRemoved
		{
			add
			{
				lock (ReferenceRemovedEvent)
				{
					Events.AddHandler(ReferenceRemovedEvent, value);
				}
			}
			remove
			{
				lock (ReferenceRemovedEvent)
				{
					Events.RemoveHandler(ReferenceRemovedEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ReferenceRemoved" /> event.
		/// </summary>
		/// <param name="e"><see cref="SystemPathEventArgs" /> object that provides the arguments for the event.</param>
		protected virtual void OnReferenceRemoved(SystemPathEventArgs e)
		{
			EventHandler<SystemPathEventArgs> handler = null;

			lock (ReferenceRemovedEvent)
			{
				handler = (EventHandler<SystemPathEventArgs>) Events[ReferenceRemovedEvent];
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when a reference changes.
		/// </summary>
		[Category("Action")]
		[Description("Event raised when a reference changes.")]
		public event EventHandler<SystemPathEventArgs> ReferenceChanged
		{
			add
			{
				lock (ReferenceChangedEvent)
				{
					Events.AddHandler(ReferenceChangedEvent, value);
				}
			}
			remove
			{
				lock (ReferenceChangedEvent)
				{
					Events.RemoveHandler(ReferenceChangedEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ReferenceChanged" /> event.
		/// </summary>
		/// <param name="e"><see cref="SystemPathEventArgs" /> object that provides the arguments for the event.</param>
		protected virtual void OnReferenceChanged(SystemPathEventArgs e)
		{
			EventHandler<SystemPathEventArgs> handler = null;

			lock (ReferenceChangedEvent)
			{
				handler = (EventHandler<SystemPathEventArgs>) Events[ReferenceChangedEvent];
			}

			if (handler != null)
				handler(this, e);
		}

		/// <summary>
		/// Event raised when a reference is added.
		/// </summary>
		[Category("Action")]
		[Description("Event raised when a reference is added.")]
		public event EventHandler<SystemPathEventArgs> ReferenceAdded
		{
			add
			{
				lock (ReferenceAddedEvent)
				{
					Events.AddHandler(ReferenceAddedEvent, value);
				}
			}
			remove
			{
				lock (ReferenceAddedEvent)
				{
					Events.RemoveHandler(ReferenceAddedEvent, value);
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="ReferenceAdded" /> event.
		/// </summary>
		/// <param name="e"><see cref="SystemPathEventArgs" /> object that provides the arguments for the event.</param>
		protected virtual void OnReferenceAdded(SystemPathEventArgs e)
		{
			EventHandler<SystemPathEventArgs> handler = null;

			lock (ReferenceAddedEvent)
			{
				handler = (EventHandler<SystemPathEventArgs>) Events[ReferenceAddedEvent];
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region Event Handlers
		private void referenceEvents_ReferenceRemoved(Reference pReference)
		{
			// NOTE: Don't use the project cache when verifying sources here since reference events may occur 
			// while a project is being removed or added from the solution
			if (pReference != null && IsSourceReference(pReference, false))
			{
				SystemPath? path = CreateSystemPath(pReference);

				if (path.HasValue)
				{
					System.Diagnostics.Debug.WriteLine("Source project removed: " + pReference.Name, "VSDocProject");

					OnReferenceRemoved(new SystemPathEventArgs(this, path.Value));
				}
			}
		}

		private void referenceEvents_ReferenceChanged(Reference pReference)
		{
			// NOTE: Don't use the project cache when verifying sources here since reference events may occur 
			// while a project is being removed or added from the solution
			if (pReference != null && IsSourceReference(pReference, false))
			{
				SystemPath? path = CreateSystemPath(pReference);

				if (path.HasValue)
				{
					System.Diagnostics.Debug.WriteLine("Source project reference changed: " + pReference.Name, "VSDocProject");

					OnReferenceChanged(new SystemPathEventArgs(this, path.Value));
				}
			}
		}

		private void referenceEvents_ReferenceAdded(Reference pReference)
		{
			// NOTE: Don't use the project cache when verifying sources here since reference events may occur 
			// while a project is being removed or added from the solution
			if (pReference != null && IsSourceReference(pReference, false))
			{
				SystemPath? path = CreateSystemPath(pReference);

				if (!path.HasValue)
				{
					System.Diagnostics.Debug.WriteLine("Source project added: " + pReference.Name, "VSDocProject");

					OnReferenceAdded(new SystemPathEventArgs(this, path.Value));
				}
			}
		}

		private void externalSources_ItemRemoved(object sender, SourceEventArgs e)
		{
			OnReferenceRemoved(new SystemPathEventArgs(this, e.Source.Input));
		}

		private void externalSources_ItemAdded(object sender, SourceEventArgs e)
		{
			OnReferenceAdded(new SystemPathEventArgs(this, e.Source.Input));
		}
		#endregion
	}
}
